MCP Builder: Production-Ready Server Development Guide
Table of Contents
- MCP Fundamentals
- Tool Design Principles
- Implementation Patterns
- Error Handling
- Testing Strategy
- Security Best Practices
- Documentation Standards
- Production Readiness
- Common Pitfalls
- Real-World Examples
MCP Fundamentals
Protocol Architecture
MCP (Model Context Protocol) is a standardized protocol for connecting AI assistants to external data sources and tools. It uses JSON-RPC 2.0 over stdio, HTTP, or SSE transports.
Key Components:
- Server: Exposes tools, resources, and prompts
- Client: Claude Desktop or other MCP clients
- Transport Layer: Communication mechanism (stdio, HTTP, SSE)
- Protocol: JSON-RPC 2.0 messages
Transport Layers
Stdio (Standard Input/Output)
- Best for: Local development, CLI tools
- Pros: Simple, no network setup
- Cons: Single client, no concurrent connections
HTTP
- Best for: Web services, remote servers
- Pros: Scalable, standard protocol
- Cons: Requires HTTP server setup
SSE (Server-Sent Events)
- Best for: Real-time updates, streaming
- Pros: Push updates, efficient
- Cons: One-way from server to client
When to Use MCP vs Alternatives
Use MCP when:
- Building tools for Claude Desktop
- Need standardized tool interface
- Want protocol-level compatibility
- Require resource discovery
Consider alternatives when:
- Building custom integrations
- Need proprietary protocols
- Performance is critical (direct API calls)
- Legacy system integration
Client-Server Communication Patterns
Request-Response Pattern
python1# FastMCP - Tool execution 2@mcp.tool() 3async def get_user_info(user_id: str) -> dict: 4 """Fetch user information""" 5 return {"id": user_id, "name": "John"}
typescript1// MCP SDK - Tool execution 2server.setRequestHandler(ListToolsRequestSchema, async () => ({ 3 tools: [{ 4 name: "get_user_info", 5 description: "Fetch user information", 6 inputSchema: { 7 type: "object", 8 properties: { 9 user_id: { type: "string" } 10 } 11 } 12 }] 13}));
Tool Design Principles
Atomic Operations
✅ GOOD: Atomic, Single Responsibility
python1# FastMCP 2@mcp.tool() 3async def create_issue(title: str, body: str, repo: str) -> dict: 4 """Create a single GitHub issue""" 5 # One clear action 6 pass 7 8@mcp.tool() 9async def update_issue(issue_id: int, title: str = None, body: str = None) -> dict: 10 """Update an existing GitHub issue""" 11 # Separate update operation 12 pass
typescript1// MCP SDK 2const createIssue = { 3 name: "create_issue", 4 description: "Create a single GitHub issue in a repository", 5 inputSchema: { 6 type: "object", 7 required: ["title", "body", "repo"], 8 properties: { 9 title: { type: "string", description: "Issue title" }, 10 body: { type: "string", description: "Issue body" }, 11 repo: { type: "string", description: "Repository name" } 12 } 13 } 14};
❌ BAD: Multi-purpose, Complex
python1# BAD: Too many responsibilities 2@mcp.tool() 3async def manage_issue(action: str, **kwargs) -> dict: 4 """Create, update, delete, or list issues""" 5 # Too many operations in one tool 6 pass
Clear Input/Output Contracts
✅ GOOD: Explicit Types and Validation
python1# FastMCP with Pydantic 2from pydantic import BaseModel, Field, validator 3 4class CreateIssueInput(BaseModel): 5 title: str = Field(..., min_length=1, max_length=200) 6 body: str = Field(..., min_length=1) 7 repo: str = Field(..., pattern=r"^[\w\-\.]+/[\w\-\.]+$") 8 labels: list[str] = Field(default_factory=list, max_items=10) 9 10@mcp.tool() 11async def create_issue(input: CreateIssueInput) -> dict: 12 """Create a GitHub issue with validated inputs""" 13 return { 14 "id": 123, 15 "title": input.title, 16 "url": f"https://github.com/{input.repo}/issues/123" 17 }
typescript1// MCP SDK with Zod validation 2import { z } from "zod"; 3 4const CreateIssueSchema = z.object({ 5 title: z.string().min(1).max(200), 6 body: z.string().min(1), 7 repo: z.string().regex(/^[\w\-\.]+\/[\w\-\.]+$/), 8 labels: z.array(z.string()).max(10).default([]) 9}); 10 11server.setRequestHandler(CallToolRequestSchema, async (request) => { 12 if (request.params.name === "create_issue") { 13 const input = CreateIssueSchema.parse(request.params.arguments); 14 // Process validated input 15 return { 16 content: [{ 17 type: "text", 18 text: JSON.stringify({ id: 123, title: input.title }) 19 }] 20 }; 21 } 22});
Parameter Validation
✅ GOOD: Comprehensive Validation
python1# FastMCP 2from typing import Literal 3from pydantic import BaseModel, validator, HttpUrl 4 5class SearchParams(BaseModel): 6 query: str = Field(..., min_length=1, max_length=500) 7 limit: int = Field(default=10, ge=1, le=100) 8 sort: Literal["relevance", "date", "stars"] = "relevance" 9 10 @validator("query") 11 def validate_query(cls, v): 12 if any(char in v for char in ["<", ">", "&"]): 13 raise ValueError("Query contains invalid characters") 14 return v.strip()
typescript1// MCP SDK 2const SearchSchema = z.object({ 3 query: z.string().min(1).max(500).refine( 4 (val) => !/[<>&]/.test(val), 5 "Query contains invalid characters" 6 ), 7 limit: z.number().int().min(1).max(100).default(10), 8 sort: z.enum(["relevance", "date", "stars"]).default("relevance") 9});
Descriptive Naming Conventions
✅ GOOD: Clear, Action-Oriented Names
python1# FastMCP 2@mcp.tool() 3async def fetch_repository_details(owner: str, repo_name: str) -> dict: 4 """Fetch detailed information about a GitHub repository""" 5 pass 6 7@mcp.tool() 8async def list_open_pull_requests(owner: str, repo_name: str) -> list[dict]: 9 """List all open pull requests in a repository""" 10 pass
❌ BAD: Vague Names
python1# BAD: Unclear purpose 2@mcp.tool() 3async def get_data(owner: str, repo: str) -> dict: 4 """Get data""" 5 pass
When to Split vs Combine Tools
Decision Tree:
- Different operations? → Split (create vs update vs delete)
- Different data sources? → Split (GitHub vs GitLab)
- Different authentication? → Split
- Same operation, different filters? → Combine with parameters
- Related operations in same transaction? → Consider combining
✅ GOOD: Appropriate Splitting
python1# FastMCP - Separate tools for different operations 2@mcp.tool() 3async def create_document(title: str, content: str) -> dict: 4 """Create a new Notion page""" 5 pass 6 7@mcp.tool() 8async def update_document(page_id: str, title: str = None, content: str = None) -> dict: 9 """Update an existing Notion page""" 10 pass 11 12@mcp.tool() 13async def delete_document(page_id: str) -> dict: 14 """Delete a Notion page""" 15 pass
Implementation Patterns
REST API Integration
FastMCP Example:
python1import httpx 2from fastmcp import FastMCP 3from pydantic import BaseModel, HttpUrl 4 5mcp = FastMCP("GitHub MCP Server") 6 7class GitHubIssue(BaseModel): 8 title: str 9 body: str 10 labels: list[str] = [] 11 12@mcp.tool() 13async def create_github_issue( 14 owner: str, 15 repo: str, 16 title: str, 17 body: str, 18 token: str 19) -> dict: 20 """Create a GitHub issue""" 21 async with httpx.AsyncClient() as client: 22 response = await client.post( 23 f"https://api.github.com/repos/{owner}/{repo}/issues", 24 json={"title": title, "body": body}, 25 headers={"Authorization": f"token {token}"}, 26 timeout=30.0 27 ) 28 response.raise_for_status() 29 return response.json() 30 31if __name__ == "__main__": 32 mcp.run()
MCP SDK Example:
typescript1import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3import fetch from "node-fetch"; 4 5const server = new Server({ 6 name: "github-mcp-server", 7 version: "1.0.0" 8}, { 9 capabilities: { 10 tools: {} 11 } 12}); 13 14server.setRequestHandler(CallToolRequestSchema, async (request) => { 15 if (request.params.name === "create_github_issue") { 16 const { owner, repo, title, body, token } = request.params.arguments as any; 17 18 const response = await fetch( 19 `https://api.github.com/repos/${owner}/${repo}/issues`, 20 { 21 method: "POST", 22 headers: { 23 "Authorization": `token ${token}`, 24 "Content-Type": "application/json" 25 }, 26 body: JSON.stringify({ title, body }), 27 signal: AbortSignal.timeout(30000) 28 } 29 ); 30 31 if (!response.ok) { 32 throw new Error(`GitHub API error: ${response.statusText}`); 33 } 34 35 const data = await response.json(); 36 return { 37 content: [{ type: "text", text: JSON.stringify(data) }] 38 }; 39 } 40}); 41 42async function main() { 43 const transport = new StdioServerTransport(); 44 await server.connect(transport); 45} 46 47main().catch(console.error);
Database Connections
FastMCP Example:
python1import asyncpg 2from fastmcp import FastMCP 3from contextlib import asynccontextmanager 4 5mcp = FastMCP("Database MCP Server") 6pool: asyncpg.Pool = None 7 8@asynccontextmanager 9async def lifespan(app): 10 global pool 11 pool = await asyncpg.create_pool( 12 "postgresql://user:pass@localhost/db", 13 min_size=2, 14 max_size=10 15 ) 16 yield 17 await pool.close() 18 19mcp.lifespan = lifespan 20 21@mcp.tool() 22async def query_users(limit: int = 10) -> list[dict]: 23 """Query users from database""" 24 async with pool.acquire() as conn: 25 rows = await conn.fetch( 26 "SELECT id, name, email FROM users LIMIT $1", 27 limit 28 ) 29 return [dict(row) for row in rows]
MCP SDK Example:
typescript1import { Pool } from "pg"; 2import { promisify } from "util"; 3 4const pool = new Pool({ 5 connectionString: "postgresql://user:pass@localhost/db", 6 min: 2, 7 max: 10 8}); 9 10server.setRequestHandler(CallToolRequestSchema, async (request) => { 11 if (request.params.name === "query_users") { 12 const { limit = 10 } = request.params.arguments as any; 13 const client = await pool.connect(); 14 try { 15 const result = await client.query( 16 "SELECT id, name, email FROM users LIMIT $1", 17 [limit] 18 ); 19 return { 20 content: [{ 21 type: "text", 22 text: JSON.stringify(result.rows) 23 }] 24 }; 25 } finally { 26 client.release(); 27 } 28 } 29});
File System Operations
FastMCP Example:
python1from pathlib import Path 2from fastmcp import FastMCP 3 4mcp = FastMCP("File System MCP Server") 5 6@mcp.tool() 7async def read_file(file_path: str) -> dict: 8 """Read file contents safely""" 9 path = Path(file_path).resolve() 10 11 # Security: Prevent directory traversal 12 if not str(path).startswith(str(Path.cwd())): 13 raise ValueError("Access denied: path outside workspace") 14 15 if not path.exists(): 16 raise FileNotFoundError(f"File not found: {file_path}") 17 18 if not path.is_file(): 19 raise ValueError(f"Path is not a file: {file_path}") 20 21 return { 22 "content": path.read_text(), 23 "size": path.stat().st_size 24 }
MCP SDK Example:
typescript1import * as fs from "fs/promises"; 2import * as path from "path"; 3 4server.setRequestHandler(CallToolRequestSchema, async (request) => { 5 if (request.params.name === "read_file") { 6 const { file_path } = request.params.arguments as any; 7 const resolved = path.resolve(file_path); 8 const cwd = process.cwd(); 9 10 // Security: Prevent directory traversal 11 if (!resolved.startsWith(cwd)) { 12 throw new Error("Access denied: path outside workspace"); 13 } 14 15 const stats = await fs.stat(resolved); 16 if (!stats.isFile()) { 17 throw new Error("Path is not a file"); 18 } 19 20 const content = await fs.readFile(resolved, "utf-8"); 21 return { 22 content: [{ 23 type: "text", 24 text: JSON.stringify({ content, size: stats.size }) 25 }] 26 }; 27 } 28});
Authentication Flows
FastMCP Example:
python1import os 2from fastmcp import FastMCP 3from typing import Optional 4 5mcp = FastMCP("Authenticated API Server") 6 7class AuthManager: 8 def __init__(self): 9 self._token: Optional[str] = None 10 11 def get_token(self) -> str: 12 if not self._token: 13 self._token = os.getenv("API_TOKEN") 14 if not self._token: 15 raise ValueError("API_TOKEN environment variable not set") 16 return self._token 17 18 def refresh_token(self): 19 self._token = None 20 21auth = AuthManager() 22 23@mcp.tool() 24async def make_authenticated_request(endpoint: str) -> dict: 25 """Make authenticated API request""" 26 token = auth.get_token() 27 # Use token in request 28 pass
MCP SDK Example:
typescript1class AuthManager { 2 private token: string | null = null; 3 4 getToken(): string { 5 if (!this.token) { 6 this.token = process.env.API_TOKEN || null; 7 if (!this.token) { 8 throw new Error("API_TOKEN environment variable not set"); 9 } 10 } 11 return this.token; 12 } 13 14 refreshToken(): void { 15 this.token = null; 16 } 17} 18 19const auth = new AuthManager();
Proper Async/Await Handling
✅ GOOD: Proper Async Patterns
python1# FastMCP 2import asyncio 3import httpx 4 5@mcp.tool() 6async def fetch_multiple_resources(urls: list[str]) -> list[dict]: 7 """Fetch multiple resources concurrently""" 8 async with httpx.AsyncClient() as client: 9 tasks = [client.get(url, timeout=10.0) for url in urls] 10 responses = await asyncio.gather(*tasks, return_exceptions=True) 11 12 results = [] 13 for response in responses: 14 if isinstance(response, Exception): 15 results.append({"error": str(response)}) 16 else: 17 response.raise_for_status() 18 results.append(response.json()) 19 return results
typescript1// MCP SDK 2async function fetchMultipleResources(urls: string[]): Promise<any[]> { 3 const promises = urls.map(url => 4 fetch(url, { signal: AbortSignal.timeout(10000) }) 5 .then(r => r.ok ? r.json() : { error: r.statusText }) 6 .catch(err => ({ error: err.message })) 7 ); 8 return Promise.all(promises); 9}
Error Handling
Explicit Error Types
FastMCP Example:
python1class MCPError(Exception): 2 """Base MCP error""" 3 pass 4 5class ValidationError(MCPError): 6 """Input validation error""" 7 pass 8 9class APIError(MCPError): 10 """External API error""" 11 def __init__(self, message: str, status_code: int = None): 12 super().__init__(message) 13 self.status_code = status_code 14 15class RateLimitError(APIError): 16 """Rate limit exceeded""" 17 pass 18 19@mcp.tool() 20async def api_call(endpoint: str) -> dict: 21 """Make API call with proper error handling""" 22 try: 23 # API call logic 24 pass 25 except httpx.HTTPStatusError as e: 26 if e.response.status_code == 429: 27 raise RateLimitError("Rate limit exceeded", 429) 28 raise APIError(f"API error: {e.response.status_code}", e.response.status_code) 29 except httpx.RequestError as e: 30 raise APIError(f"Request failed: {str(e)}")
MCP SDK Example:
typescript1class MCPError extends Error { 2 constructor(message: string, public statusCode?: number) { 3 super(message); 4 this.name = "MCPError"; 5 } 6} 7 8class ValidationError extends MCPError {} 9class APIError extends MCPError {} 10class RateLimitError extends APIError {} 11 12server.setRequestHandler(CallToolRequestSchema, async (request) => { 13 try { 14 // Tool logic 15 } catch (error: any) { 16 if (error.response?.status === 429) { 17 throw new RateLimitError("Rate limit exceeded", 429); 18 } 19 if (error.response) { 20 throw new APIError(`API error: ${error.response.status}`, error.response.status); 21 } 22 throw new MCPError(`Request failed: ${error.message}`); 23 } 24});
User-Facing Error Messages
✅ GOOD: Clear, Actionable Messages
python1@mcp.tool() 2async def create_issue(title: str, repo: str) -> dict: 3 """Create GitHub issue""" 4 if not title.strip(): 5 raise ValueError( 6 "Issue title cannot be empty. Please provide a descriptive title." 7 ) 8 9 if "/" not in repo: 10 raise ValueError( 11 f"Invalid repository format: '{repo}'. Expected format: 'owner/repo-name'" 12 )
❌ BAD: Technical, Unhelpful
python1# BAD 2raise ValueError("Invalid input") 3raise Exception("Error occurred")
Retry Logic
FastMCP Example:
python1import asyncio 2from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type 3 4@retry( 5 stop=stop_after_attempt(3), 6 wait=wait_exponential(multiplier=1, min=2, max=10), 7 retry=retry_if_exception_type((httpx.RequestError, RateLimitError)) 8) 9async def api_call_with_retry(url: str) -> dict: 10 """API call with exponential backoff retry""" 11 async with httpx.AsyncClient() as client: 12 response = await client.get(url, timeout=30.0) 13 if response.status_code == 429: 14 raise RateLimitError("Rate limited") 15 response.raise_for_status() 16 return response.json()
MCP SDK Example:
typescript1async function retry<T>( 2 fn: () => Promise<T>, 3 maxAttempts: number = 3, 4 delay: number = 1000 5): Promise<T> { 6 for (let i = 0; i < maxAttempts; i++) { 7 try { 8 return await fn(); 9 } catch (error) { 10 if (i === maxAttempts - 1) throw error; 11 if (error instanceof RateLimitError) { 12 await new Promise(r => setTimeout(r, delay * Math.pow(2, i))); 13 } else { 14 throw error; 15 } 16 } 17 } 18 throw new Error("Retry failed"); 19}
Timeout Handling
FastMCP Example:
python1import asyncio 2 3@mcp.tool() 4async def long_running_operation(timeout_seconds: int = 30) -> dict: 5 """Operation with timeout""" 6 try: 7 return await asyncio.wait_for( 8 perform_operation(), 9 timeout=timeout_seconds 10 ) 11 except asyncio.TimeoutError: 12 raise TimeoutError( 13 f"Operation timed out after {timeout_seconds} seconds. " 14 "Please try again or reduce the scope of the operation." 15 )
MCP SDK Example:
typescript1async function withTimeout<T>( 2 promise: Promise<T>, 3 timeoutMs: number 4): Promise<T> { 5 return Promise.race([ 6 promise, 7 new Promise<T>((_, reject) => 8 setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs) 9 ) 10 ]); 11}
Graceful Degradation
FastMCP Example:
python1@mcp.tool() 2async def fetch_with_fallback(primary_url: str, fallback_url: str) -> dict: 3 """Fetch with fallback on failure""" 4 try: 5 async with httpx.AsyncClient() as client: 6 response = await client.get(primary_url, timeout=10.0) 7 response.raise_for_status() 8 return response.json() 9 except Exception as e: 10 logger.warning(f"Primary source failed: {e}, trying fallback") 11 try: 12 async with httpx.AsyncClient() as client: 13 response = await client.get(fallback_url, timeout=10.0) 14 response.raise_for_status() 15 return response.json() 16 except Exception as fallback_error: 17 raise APIError( 18 f"Both primary and fallback sources failed. " 19 f"Primary: {str(e)}, Fallback: {str(fallback_error)}" 20 )
Logging Strategies
FastMCP Example:
python1import logging 2import sys 3 4logging.basicConfig( 5 level=logging.INFO, 6 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 7 handlers=[ 8 logging.FileHandler("mcp-server.log"), 9 logging.StreamHandler(sys.stderr) 10 ] 11) 12 13logger = logging.getLogger("mcp_server") 14 15@mcp.tool() 16async def tool_with_logging(param: str) -> dict: 17 """Tool with structured logging""" 18 logger.info(f"Tool called with param: {param[:50]}") # Truncate for security 19 try: 20 result = await perform_operation(param) 21 logger.info(f"Tool completed successfully") 22 return result 23 except Exception as e: 24 logger.error(f"Tool failed: {str(e)}", exc_info=True) 25 raise
MCP SDK Example:
typescript1import * as winston from "winston"; 2 3const logger = winston.createLogger({ 4 level: "info", 5 format: winston.format.combine( 6 winston.format.timestamp(), 7 winston.format.json() 8 ), 9 transports: [ 10 new winston.transports.File({ filename: "mcp-server.log" }), 11 new winston.transports.Console() 12 ] 13}); 14 15server.setRequestHandler(CallToolRequestSchema, async (request) => { 16 logger.info("Tool called", { tool: request.params.name }); 17 try { 18 // Tool logic 19 logger.info("Tool completed"); 20 } catch (error) { 21 logger.error("Tool failed", { error: error.message, stack: error.stack }); 22 throw error; 23 } 24});
Testing Strategy
Unit Tests for Individual Tools
FastMCP Example:
python1import pytest 2from unittest.mock import AsyncMock, patch 3from mcp_server import create_issue 4 5@pytest.mark.asyncio 6async def test_create_issue_success(): 7 """Test successful issue creation""" 8 with patch("httpx.AsyncClient") as mock_client: 9 mock_response = AsyncMock() 10 mock_response.json.return_value = {"id": 123, "title": "Test"} 11 mock_response.raise_for_status = AsyncMock() 12 13 mock_client.return_value.__aenter__.return_value.post.return_value = mock_response 14 15 result = await create_issue( 16 owner="test", 17 repo="repo", 18 title="Test Issue", 19 body="Body", 20 token="token" 21 ) 22 23 assert result["id"] == 123 24 assert result["title"] == "Test" 25 26@pytest.mark.asyncio 27async def test_create_issue_validation_error(): 28 """Test validation error handling""" 29 with pytest.raises(ValueError, match="title cannot be empty"): 30 await create_issue("test", "repo", "", "body", "token")
MCP SDK Example:
typescript1import { describe, it, expect, vi } from "vitest"; 2import { createIssue } from "./tools"; 3 4describe("createIssue", () => { 5 it("should create issue successfully", async () => { 6 global.fetch = vi.fn().mockResolvedValue({ 7 ok: true, 8 json: async () => ({ id: 123, title: "Test" }) 9 }); 10 11 const result = await createIssue({ 12 owner: "test", 13 repo: "repo", 14 title: "Test Issue", 15 body: "Body", 16 token: "token" 17 }); 18 19 expect(result.id).toBe(123); 20 }); 21 22 it("should throw validation error for empty title", async () => { 23 await expect( 24 createIssue({ owner: "test", repo: "repo", title: "", body: "body", token: "token" }) 25 ).rejects.toThrow("title cannot be empty"); 26 }); 27});
Integration Tests with Mock Servers
FastMCP Example:
python1import pytest 2from httpx import ASGITransport, AsyncClient 3from mcp_server import app 4 5@pytest.mark.asyncio 6async def test_integration_create_issue(): 7 """Integration test with mock server""" 8 async with AsyncClient( 9 transport=ASGITransport(app=app), 10 base_url="http://test" 11 ) as client: 12 response = await client.post( 13 "/tools/call", 14 json={ 15 "name": "create_issue", 16 "arguments": { 17 "owner": "test", 18 "repo": "repo", 19 "title": "Test", 20 "body": "Body" 21 } 22 } 23 ) 24 assert response.status_code == 200 25 data = response.json() 26 assert "id" in data
MCP SDK Example:
typescript1import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2import { TestTransport } from "./test-transport"; 3 4describe("Integration Tests", () => { 5 it("should handle tool call end-to-end", async () => { 6 const transport = new TestTransport(); 7 await server.connect(transport); 8 9 const response = await transport.sendRequest({ 10 jsonrpc: "2.0", 11 id: 1, 12 method: "tools/call", 13 params: { 14 name: "create_issue", 15 arguments: { 16 owner: "test", 17 repo: "repo", 18 title: "Test", 19 body: "Body" 20 } 21 } 22 }); 23 24 expect(response.result).toHaveProperty("content"); 25 }); 26});
Validation of Tool Schemas
FastMCP Example:
python1import pytest 2from pydantic import ValidationError 3from mcp_server import CreateIssueInput 4 5def test_tool_schema_validation(): 6 """Test tool input schema validation""" 7 # Valid input 8 valid = CreateIssueInput( 9 title="Test", 10 body="Body", 11 repo="owner/repo" 12 ) 13 assert valid.title == "Test" 14 15 # Invalid input 16 with pytest.raises(ValidationError): 17 CreateIssueInput(title="", body="Body", repo="owner/repo") 18 19 # Invalid repo format 20 with pytest.raises(ValidationError): 21 CreateIssueInput(title="Test", body="Body", repo="invalid")
MCP SDK Example:
typescript1import { describe, it, expect } from "vitest"; 2import { CreateIssueSchema } from "./schemas"; 3 4describe("Tool Schema Validation", () => { 5 it("should validate correct input", () => { 6 const result = CreateIssueSchema.parse({ 7 title: "Test", 8 body: "Body", 9 repo: "owner/repo" 10 }); 11 expect(result.title).toBe("Test"); 12 }); 13 14 it("should reject invalid input", () => { 15 expect(() => { 16 CreateIssueSchema.parse({ title: "", body: "Body", repo: "owner/repo" }); 17 }).toThrow(); 18 }); 19});
Testing Authentication Flows
FastMCP Example:
python1@pytest.mark.asyncio 2async def test_authentication_success(): 3 """Test successful authentication""" 4 with patch.dict(os.environ, {"API_TOKEN": "test-token"}): 5 auth = AuthManager() 6 token = auth.get_token() 7 assert token == "test-token" 8 9@pytest.mark.asyncio 10async def test_authentication_missing_token(): 11 """Test missing token error""" 12 with patch.dict(os.environ, {}, clear=True): 13 auth = AuthManager() 14 with pytest.raises(ValueError, match="API_TOKEN"): 15 auth.get_token()
Handling Rate Limits in Tests
FastMCP Example:
python1@pytest.mark.asyncio 2async def test_rate_limit_handling(): 3 """Test rate limit retry logic""" 4 call_count = 0 5 6 async def mock_api_call(): 7 nonlocal call_count 8 call_count += 1 9 if call_count < 3: 10 raise RateLimitError("Rate limited", 429) 11 return {"success": True} 12 13 result = await api_call_with_retry("http://test.com") 14 assert result["success"] is True 15 assert call_count == 3
Security Best Practices
API Key Management
✅ GOOD: Environment Variables
python1# FastMCP 2import os 3from fastmcp import FastMCP 4 5mcp = FastMCP("Secure Server") 6 7# Never hardcode keys 8API_KEY = os.getenv("API_KEY") 9if not API_KEY: 10 raise ValueError("API_KEY environment variable must be set") 11 12@mcp.tool() 13async def secure_api_call() -> dict: 14 """Make secure API call""" 15 # Use environment variable 16 headers = {"Authorization": f"Bearer {API_KEY}"} 17 # ...
typescript1// MCP SDK 2const API_KEY = process.env.API_KEY; 3if (!API_KEY) { 4 throw new Error("API_KEY environment variable must be set"); 5} 6 7// Use API_KEY in requests
❌ BAD: Hardcoded Secrets
python1# NEVER DO THIS 2API_KEY = "sk-1234567890abcdef"
Input Sanitization
FastMCP Example:
python1import html 2import re 3from pydantic import validator 4 5class SafeInput(BaseModel): 6 user_input: str 7 8 @validator("user_input") 9 def sanitize_input(cls, v): 10 # Remove potentially dangerous characters 11 v = html.escape(v) 12 # Remove script tags 13 v = re.sub(r"<script[^>]*>.*?</script>", "", v, flags=re.IGNORECASE | re.DOTALL) 14 # Limit length 15 if len(v) > 10000: 16 raise ValueError("Input too long") 17 return v.strip()
MCP SDK Example:
typescript1import { z } from "zod"; 2import DOMPurify from "isomorphic-dompurify"; 3 4const SafeInputSchema = z.object({ 5 userInput: z.string() 6 .max(10000, "Input too long") 7 .transform(val => DOMPurify.sanitize(val.trim())) 8});
Preventing Injection Attacks
✅ GOOD: Parameterized Queries
python1# FastMCP - Database 2async def safe_query(user_id: str) -> list[dict]: 3 """Safe database query with parameterization""" 4 async with pool.acquire() as conn: 5 # Use parameterized query 6 rows = await conn.fetch( 7 "SELECT * FROM users WHERE id = $1", 8 user_id # Safe: parameterized 9 ) 10 return [dict(row) for row in rows]
❌ BAD: String Concatenation
python1# NEVER DO THIS - SQL Injection risk 2query = f"SELECT * FROM users WHERE id = '{user_id}'"
Rate Limiting
FastMCP Example:
python1from collections import defaultdict 2from datetime import datetime, timedelta 3import asyncio 4 5class RateLimiter: 6 def __init__(self, max_requests: int, window_seconds: int): 7 self.max_requests = max_requests 8 self.window_seconds = window_seconds 9 self.requests: dict[str, list[datetime]] = defaultdict(list) 10 self.lock = asyncio.Lock() 11 12 async def check_rate_limit(self, key: str) -> bool: 13 async with self.lock: 14 now = datetime.now() 15 window_start = now - timedelta(seconds=self.window_seconds) 16 17 # Clean old requests 18 self.requests[key] = [ 19 req_time for req_time in self.requests[key] 20 if req_time > window_start 21 ] 22 23 if len(self.requests[key]) >= self.max_requests: 24 return False 25 26 self.requests[key].append(now) 27 return True 28 29rate_limiter = RateLimiter(max_requests=100, window_seconds=60) 30 31@mcp.tool() 32async def rate_limited_operation() -> dict: 33 """Operation with rate limiting""" 34 if not await rate_limiter.check_rate_limit("default"): 35 raise RateLimitError("Rate limit exceeded. Please try again later.") 36 # Proceed with operation
MCP SDK Example:
typescript1class RateLimiter { 2 private requests: Map<string, number[]> = new Map(); 3 4 constructor( 5 private maxRequests: number, 6 private windowMs: number 7 ) {} 8 9 check(key: string): boolean { 10 const now = Date.now(); 11 const windowStart = now - this.windowMs; 12 13 const timestamps = this.requests.get(key) || []; 14 const recent = timestamps.filter(ts => ts > windowStart); 15 16 if (recent.length >= this.maxRequests) { 17 return false; 18 } 19 20 recent.push(now); 21 this.requests.set(key, recent); 22 return true; 23 } 24}
Scoping Permissions Appropriately
✅ GOOD: Principle of Least Privilege
python1# FastMCP - Scoped permissions 2@mcp.tool() 3async def read_user_data(user_id: str, requester_id: str) -> dict: 4 """Read user data with permission check""" 5 # Check if requester has permission 6 if requester_id != user_id: 7 # Check if requester is admin 8 if not await is_admin(requester_id): 9 raise PermissionError("Insufficient permissions") 10 11 return await get_user_data(user_id)
Documentation Standards
Tool Descriptions for LLMs
✅ GOOD: Clear, Action-Oriented Descriptions
python1# FastMCP 2@mcp.tool() 3async def create_github_issue( 4 owner: str, 5 repo: str, 6 title: str, 7 body: str 8) -> dict: 9 """ 10 Create a new GitHub issue in the specified repository. 11 12 Use this tool when you need to create a bug report, feature request, 13 or any other type of issue in a GitHub repository. The issue will be 14 created with the provided title and body text. 15 16 Args: 17 owner: GitHub username or organization name (e.g., "microsoft") 18 repo: Repository name (e.g., "vscode") 19 title: Issue title (required, 1-200 characters) 20 body: Issue description/body text (required, supports Markdown) 21 22 Returns: 23 Dictionary containing: 24 - id: Issue number 25 - url: URL to the created issue 26 - title: Issue title 27 - state: Issue state (usually "open") 28 29 Raises: 30 ValueError: If title or body is empty 31 APIError: If GitHub API request fails 32 RateLimitError: If GitHub rate limit is exceeded 33 34 Example: 35 >>> create_github_issue( 36 ... owner="microsoft", 37 ... repo="vscode", 38 ... title="Bug: Editor crashes", 39 ... body="The editor crashes when..." 40 ... ) 41 {"id": 12345, "url": "https://github.com/microsoft/vscode/issues/12345", ...} 42 """ 43 pass
❌ BAD: Vague Descriptions
python1# BAD 2@mcp.tool() 3async def do_stuff(param: str) -> dict: 4 """Does stuff""" 5 pass
Parameter Documentation
FastMCP Example:
python1from pydantic import Field 2 3@mcp.tool() 4async def search_repositories( 5 query: str = Field( 6 ..., 7 description="Search query string. Supports GitHub search syntax.", 8 examples=["language:python", "stars:>1000"] 9 ), 10 sort: str = Field( 11 default="best-match", 12 description="Sort order: 'best-match', 'stars', 'forks', 'updated'", 13 examples=["stars", "updated"] 14 ), 15 limit: int = Field( 16 default=10, 17 ge=1, 18 le=100, 19 description="Maximum number of results to return (1-100)" 20 ) 21) -> dict: 22 """Search GitHub repositories with detailed parameter docs""" 23 pass
MCP SDK Example:
typescript1const searchRepositories = { 2 name: "search_repositories", 3 description: "Search GitHub repositories using GitHub's search API", 4 inputSchema: { 5 type: "object", 6 required: ["query"], 7 properties: { 8 query: { 9 type: "string", 10 description: "Search query string. Supports GitHub search syntax. Examples: 'language:python', 'stars:>1000'" 11 }, 12 sort: { 13 type: "string", 14 enum: ["best-match", "stars", "forks", "updated"], 15 default: "best-match", 16 description: "Sort order for results" 17 }, 18 limit: { 19 type: "number", 20 minimum: 1, 21 maximum: 100, 22 default: 10, 23 description: "Maximum number of results to return (1-100)" 24 } 25 } 26 } 27};
Return Value Schemas
FastMCP Example:
python1from typing import TypedDict 2 3class IssueResult(TypedDict): 4 id: int 5 url: str 6 title: str 7 state: str 8 created_at: str 9 author: str 10 11@mcp.tool() 12async def get_issue(owner: str, repo: str, issue_number: int) -> IssueResult: 13 """ 14 Get GitHub issue details. 15 16 Returns: 17 IssueResult with fields: 18 - id: Issue number 19 - url: Full URL to issue 20 - title: Issue title 21 - state: "open" or "closed" 22 - created_at: ISO 8601 timestamp 23 - author: GitHub username of issue creator 24 """ 25 pass
Usage Examples
FastMCP Example:
python1@mcp.tool() 2async def create_notion_page( 3 database_id: str, 4 title: str, 5 content: str 6) -> dict: 7 """ 8 Create a new page in a Notion database. 9 10 Examples: 11 Basic usage: 12 >>> create_notion_page( 13 ... database_id="abc123", 14 ... title="Meeting Notes", 15 ... content="# Meeting Notes\\n\\nDiscussion points..." 16 ... ) 17 18 With rich content: 19 >>> create_notion_page( 20 ... database_id="abc123", 21 ... title="Project Plan", 22 ... content="## Phase 1\\n- [ ] Task 1\\n- [ ] Task 2" 23 ... ) 24 """ 25 pass
Common Pitfalls Documentation
FastMCP Example:
python1@mcp.tool() 2async def update_github_issue( 3 owner: str, 4 repo: str, 5 issue_number: int, 6 title: str = None, 7 body: str = None 8) -> dict: 9 """ 10 Update an existing GitHub issue. 11 12 Common Pitfalls: 13 1. Both title and body are optional, but at least one must be provided 14 2. Issue number must exist in the repository 15 3. You must have write access to the repository 16 4. Rate limits: 5000 requests/hour for authenticated requests 17 18 Error Handling: 19 - 404: Issue not found or repository doesn't exist 20 - 403: Insufficient permissions 21 - 429: Rate limit exceeded (wait and retry) 22 """ 23 if not title and not body: 24 raise ValueError("At least one of 'title' or 'body' must be provided") 25 pass
Production Readiness
Configuration Management
FastMCP Example:
python1import os 2from pydantic import BaseSettings 3from typing import Optional 4 5class Settings(BaseSettings): 6 api_key: str 7 api_base_url: str = "https://api.example.com" 8 timeout_seconds: int = 30 9 max_retries: int = 3 10 log_level: str = "INFO" 11 database_url: Optional[str] = None 12 13 class Config: 14 env_file = ".env" 15 env_file_encoding = "utf-8" 16 17settings = Settings() 18 19@mcp.tool() 20async def configured_api_call() -> dict: 21 """API call using configuration""" 22 async with httpx.AsyncClient( 23 base_url=settings.api_base_url, 24 timeout=settings.timeout_seconds 25 ) as client: 26 # Use settings.api_key 27 pass
MCP SDK Example:
typescript1import { config } from "dotenv"; 2config(); 3 4interface Config { 5 apiKey: string; 6 apiBaseUrl: string; 7 timeoutSeconds: number; 8 maxRetries: number; 9 logLevel: string; 10 databaseUrl?: string; 11} 12 13const settings: Config = { 14 apiKey: process.env.API_KEY!, 15 apiBaseUrl: process.env.API_BASE_URL || "https://api.example.com", 16 timeoutSeconds: parseInt(process.env.TIMEOUT_SECONDS || "30"), 17 maxRetries: parseInt(process.env.MAX_RETRIES || "3"), 18 logLevel: process.env.LOG_LEVEL || "INFO", 19 databaseUrl: process.env.DATABASE_URL 20};
Dependency Management
FastMCP - requirements.txt:
txt1fastmcp>=0.9.0 2httpx>=0.25.0 3pydantic>=2.0.0 4python-dotenv>=1.0.0 5tenacity>=8.2.0
MCP SDK - package.json:
json1{ 2 "name": "mcp-server", 3 "version": "1.0.0", 4 "dependencies": { 5 "@modelcontextprotocol/sdk": "^0.5.0", 6 "zod": "^3.22.0", 7 "dotenv": "^16.3.0", 8 "node-fetch": "^3.3.0" 9 }, 10 "devDependencies": { 11 "@types/node": "^20.0.0", 12 "typescript": "^5.0.0", 13 "vitest": "^1.0.0" 14 } 15}
Deployment Considerations
Dockerfile Example:
dockerfile1# FastMCP 2FROM python:3.11-slim 3 4WORKDIR /app 5 6COPY requirements.txt . 7RUN pip install --no-cache-dir -r requirements.txt 8 9COPY . . 10 11CMD ["python", "-m", "mcp_server"]
dockerfile1# MCP SDK 2FROM node:20-slim 3 4WORKDIR /app 5 6COPY package*.json ./ 7RUN npm ci --only=production 8 9COPY . . 10RUN npm run build 11 12CMD ["node", "dist/index.js"]
Monitoring and Logging
FastMCP Example:
python1import logging 2import json 3from datetime import datetime 4 5class StructuredLogger: 6 def __init__(self): 7 self.logger = logging.getLogger("mcp_server") 8 handler = logging.StreamHandler() 9 handler.setFormatter(logging.Formatter( 10 '%(message)s' # JSON format 11 )) 12 self.logger.addHandler(handler) 13 self.logger.setLevel(logging.INFO) 14 15 def log_tool_call(self, tool_name: str, duration_ms: float, success: bool): 16 self.logger.info(json.dumps({ 17 "timestamp": datetime.utcnow().isoformat(), 18 "event": "tool_call", 19 "tool": tool_name, 20 "duration_ms": duration_ms, 21 "success": success 22 }))
MCP SDK Example:
typescript1import * as winston from "winston"; 2 3const logger = winston.createLogger({ 4 format: winston.format.combine( 5 winston.format.timestamp(), 6 winston.format.json() 7 ), 8 transports: [ 9 new winston.transports.Console(), 10 new winston.transports.File({ filename: "mcp-server.log" }) 11 ] 12}); 13 14function logToolCall(toolName: string, durationMs: number, success: boolean) { 15 logger.info("tool_call", { 16 tool: toolName, 17 duration_ms: durationMs, 18 success 19 }); 20}
Versioning Strategy
FastMCP Example:
python1from fastmcp import FastMCP 2 3mcp = FastMCP( 4 "My MCP Server", 5 version="1.2.3" 6) 7 8# Semantic versioning: MAJOR.MINOR.PATCH 9# MAJOR: Breaking changes 10# MINOR: New features, backwards compatible 11# PATCH: Bug fixes, backwards compatible
MCP SDK Example:
typescript1const server = new Server({ 2 name: "my-mcp-server", 3 version: "1.2.3" // Semantic versioning 4}, { 5 capabilities: { 6 tools: {} 7 } 8});
Backwards Compatibility
✅ GOOD: Additive Changes
python1# Version 1.0 2@mcp.tool() 3async def create_issue(title: str, body: str) -> dict: 4 """Create issue""" 5 pass 6 7# Version 1.1 - Backwards compatible (new optional parameter) 8@mcp.tool() 9async def create_issue( 10 title: str, 11 body: str, 12 labels: list[str] = None # New optional parameter 13) -> dict: 14 """Create issue with optional labels""" 15 pass
❌ BAD: Breaking Changes
python1# Version 1.0 2@mcp.tool() 3async def create_issue(title: str, body: str) -> dict: 4 pass 5 6# Version 2.0 - BREAKING: Changed parameter name 7@mcp.tool() 8async def create_issue(heading: str, content: str) -> dict: # BAD 9 pass
Common Pitfalls
Anti-Patterns to Avoid
1. God Tools (Too Many Responsibilities)
python1# ❌ BAD 2@mcp.tool() 3async def manage_github(action: str, **kwargs) -> dict: 4 """Do everything GitHub-related""" 5 if action == "create_issue": 6 # ... 7 elif action == "create_pr": 8 # ... 9 elif action == "list_repos": 10 # ... 11 # Too many responsibilities!
python1# ✅ GOOD 2@mcp.tool() 3async def create_github_issue(...) -> dict: 4 """Create a GitHub issue""" 5 pass 6 7@mcp.tool() 8async def create_github_pr(...) -> dict: 9 """Create a GitHub pull request""" 10 pass 11 12@mcp.tool() 13async def list_github_repos(...) -> dict: 14 """List GitHub repositories""" 15 pass
2. Ignoring Errors
python1# ❌ BAD 2@mcp.tool() 3async def api_call() -> dict: 4 try: 5 result = await make_api_call() 6 return result 7 except: 8 return {} # Silent failure!
python1# ✅ GOOD 2@mcp.tool() 3async def api_call() -> dict: 4 try: 5 result = await make_api_call() 6 return result 7 except httpx.HTTPStatusError as e: 8 raise APIError(f"API error: {e.response.status_code}") 9 except Exception as e: 10 logger.error(f"Unexpected error: {e}", exc_info=True) 11 raise
3. No Input Validation
python1# ❌ BAD 2@mcp.tool() 3async def query_database(sql: str) -> dict: 4 # No validation - SQL injection risk! 5 return await db.execute(sql)
python1# ✅ GOOD 2@mcp.tool() 3async def query_database(table: str, filters: dict) -> dict: 4 # Validate and use parameterized queries 5 if table not in ALLOWED_TABLES: 6 raise ValueError(f"Table '{table}' not allowed") 7 # Use parameterized query 8 return await db.execute_parameterized(table, filters)
Performance Bottlenecks
1. Synchronous Operations in Async Context
python1# ❌ BAD 2@mcp.tool() 3async def read_file(file_path: str) -> dict: 4 content = open(file_path).read() # Blocking! 5 return {"content": content}
python1# ✅ GOOD 2@mcp.tool() 3async def read_file(file_path: str) -> dict: 4 content = await asyncio.to_thread( 5 lambda: Path(file_path).read_text() 6 ) 7 return {"content": content}
2. Not Using Connection Pooling
python1# ❌ BAD 2@mcp.tool() 3async def db_query() -> dict: 4 conn = await asyncpg.connect(DB_URL) # New connection each time! 5 result = await conn.fetch("SELECT * FROM users") 6 await conn.close() 7 return result
python1# ✅ GOOD 2pool = await asyncpg.create_pool(DB_URL, min_size=2, max_size=10) 3 4@mcp.tool() 5async def db_query() -> dict: 6 async with pool.acquire() as conn: # Reuse connection 7 result = await conn.fetch("SELECT * FROM users") 8 return result
Common Bugs
1. Race Conditions
python1# ❌ BAD 2counter = 0 3 4@mcp.tool() 5async def increment() -> dict: 6 global counter 7 counter += 1 # Race condition! 8 return {"count": counter}
python1# ✅ GOOD 2import asyncio 3 4counter = 0 5lock = asyncio.Lock() 6 7@mcp.tool() 8async def increment() -> dict: 9 global counter 10 async with lock: 11 counter += 1 12 return {"count": counter}
2. Resource Leaks
python1# ❌ BAD 2@mcp.tool() 3async def api_call() -> dict: 4 client = httpx.AsyncClient() # Never closed! 5 response = await client.get("https://api.example.com") 6 return response.json()
python1# ✅ GOOD 2@mcp.tool() 3async def api_call() -> dict: 4 async with httpx.AsyncClient() as client: # Auto-closed 5 response = await client.get("https://api.example.com") 6 return response.json()
Overly Complex Tool Designs
❌ BAD: Over-Engineered
python1@mcp.tool() 2async def complex_operation( 3 config: dict, 4 options: dict, 5 callbacks: list[callable], 6 middleware: list[callable] 7) -> dict: 8 """Too many layers of abstraction""" 9 # Complex nested logic 10 pass
✅ GOOD: Simple and Clear
python1@mcp.tool() 2async def create_issue(title: str, body: str, repo: str) -> dict: 3 """Create issue - simple and clear""" 4 # Straightforward implementation 5 pass
Poor Error Messages
❌ BAD:
python1raise ValueError("Error") 2raise Exception("Failed")
✅ GOOD:
python1raise ValueError( 2 "Issue title cannot be empty. Please provide a descriptive title " 3 "that summarizes the issue (1-200 characters)." 4)
Real-World Examples
GitHub API Integration
Complete FastMCP Example:
python1import os 2import httpx 3from fastmcp import FastMCP 4from pydantic import BaseModel, Field, HttpUrl 5from typing import Optional 6 7mcp = FastMCP("GitHub MCP Server") 8 9GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") 10if not GITHUB_TOKEN: 11 raise ValueError("GITHUB_TOKEN environment variable required") 12 13class CreateIssueInput(BaseModel): 14 owner: str = Field(..., description="Repository owner (username or org)") 15 repo: str = Field(..., description="Repository name") 16 title: str = Field(..., min_length=1, max_length=200) 17 body: str = Field(..., min_length=1) 18 labels: list[str] = Field(default_factory=list, max_items=20) 19 20class IssueResult(BaseModel): 21 id: int 22 number: int 23 title: str 24 url: str 25 state: str 26 created_at: str 27 28@mcp.tool() 29async def create_github_issue(input: CreateIssueInput) -> IssueResult: 30 """ 31 Create a new GitHub issue in the specified repository. 32 33 Requires GITHUB_TOKEN environment variable with 'repo' scope. 34 """ 35 async with httpx.AsyncClient() as client: 36 response = await client.post( 37 f"https://api.github.com/repos/{input.owner}/{input.repo}/issues", 38 json={ 39 "title": input.title, 40 "body": input.body, 41 "labels": input.labels 42 }, 43 headers={ 44 "Authorization": f"token {GITHUB_TOKEN}", 45 "Accept": "application/vnd.github.v3+json" 46 }, 47 timeout=30.0 48 ) 49 50 if response.status_code == 401: 51 raise ValueError("Invalid GITHUB_TOKEN. Check your token permissions.") 52 elif response.status_code == 403: 53 raise ValueError("Insufficient permissions. Token needs 'repo' scope.") 54 elif response.status_code == 404: 55 raise ValueError(f"Repository {input.owner}/{input.repo} not found.") 56 elif response.status_code == 429: 57 raise ValueError("GitHub rate limit exceeded. Please wait before retrying.") 58 59 response.raise_for_status() 60 data = response.json() 61 62 return IssueResult( 63 id=data["id"], 64 number=data["number"], 65 title=data["title"], 66 url=data["html_url"], 67 state=data["state"], 68 created_at=data["created_at"] 69 ) 70 71@mcp.tool() 72async def list_github_issues( 73 owner: str, 74 repo: str, 75 state: str = "open", 76 limit: int = 10 77) -> list[IssueResult]: 78 """List GitHub issues in a repository""" 79 async with httpx.AsyncClient() as client: 80 response = await client.get( 81 f"https://api.github.com/repos/{owner}/{repo}/issues", 82 params={"state": state, "per_page": min(limit, 100)}, 83 headers={ 84 "Authorization": f"token {GITHUB_TOKEN}", 85 "Accept": "application/vnd.github.v3+json" 86 }, 87 timeout=30.0 88 ) 89 response.raise_for_status() 90 return [IssueResult(**issue) for issue in response.json()] 91 92if __name__ == "__main__": 93 mcp.run()
Complete MCP SDK Example:
typescript1import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3import { 4 CallToolRequestSchema, 5 ListToolsRequestSchema 6} from "@modelcontextprotocol/sdk/types.js"; 7import fetch from "node-fetch"; 8import { z } from "zod"; 9 10const GITHUB_TOKEN = process.env.GITHUB_TOKEN; 11if (!GITHUB_TOKEN) { 12 throw new Error("GITHUB_TOKEN environment variable required"); 13} 14 15const CreateIssueSchema = z.object({ 16 owner: z.string().describe("Repository owner (username or org)"), 17 repo: z.string().describe("Repository name"), 18 title: z.string().min(1).max(200), 19 body: z.string().min(1), 20 labels: z.array(z.string()).max(20).default([]) 21}); 22 23const server = new Server({ 24 name: "github-mcp-server", 25 version: "1.0.0" 26}, { 27 capabilities: { 28 tools: {} 29 } 30}); 31 32server.setRequestHandler(ListToolsRequestSchema, async () => ({ 33 tools: [ 34 { 35 name: "create_github_issue", 36 description: "Create a new GitHub issue in the specified repository. Requires GITHUB_TOKEN with 'repo' scope.", 37 inputSchema: { 38 type: "object", 39 required: ["owner", "repo", "title", "body"], 40 properties: { 41 owner: { type: "string", description: "Repository owner" }, 42 repo: { type: "string", description: "Repository name" }, 43 title: { type: "string", minLength: 1, maxLength: 200 }, 44 body: { type: "string", minLength: 1 }, 45 labels: { 46 type: "array", 47 items: { type: "string" }, 48 maxItems: 20, 49 default: [] 50 } 51 } 52 } 53 }, 54 { 55 name: "list_github_issues", 56 description: "List GitHub issues in a repository", 57 inputSchema: { 58 type: "object", 59 required: ["owner", "repo"], 60 properties: { 61 owner: { type: "string" }, 62 repo: { type: "string" }, 63 state: { type: "string", enum: ["open", "closed", "all"], default: "open" }, 64 limit: { type: "number", minimum: 1, maximum: 100, default: 10 } 65 } 66 } 67 } 68 ] 69})); 70 71server.setRequestHandler(CallToolRequestSchema, async (request) => { 72 if (request.params.name === "create_github_issue") { 73 const input = CreateIssueSchema.parse(request.params.arguments); 74 75 const response = await fetch( 76 `https://api.github.com/repos/${input.owner}/${input.repo}/issues`, 77 { 78 method: "POST", 79 headers: { 80 "Authorization": `token ${GITHUB_TOKEN}`, 81 "Accept": "application/vnd.github.v3+json", 82 "Content-Type": "application/json" 83 }, 84 body: JSON.stringify({ 85 title: input.title, 86 body: input.body, 87 labels: input.labels 88 }), 89 signal: AbortSignal.timeout(30000) 90 } 91 ); 92 93 if (response.status === 401) { 94 throw new Error("Invalid GITHUB_TOKEN. Check your token permissions."); 95 } else if (response.status === 403) { 96 throw new Error("Insufficient permissions. Token needs 'repo' scope."); 97 } else if (response.status === 404) { 98 throw new Error(`Repository ${input.owner}/${input.repo} not found.`); 99 } else if (response.status === 429) { 100 throw new Error("GitHub rate limit exceeded. Please wait before retrying."); 101 } 102 103 if (!response.ok) { 104 throw new Error(`GitHub API error: ${response.statusText}`); 105 } 106 107 const data = await response.json(); 108 return { 109 content: [{ 110 type: "text", 111 text: JSON.stringify({ 112 id: data.id, 113 number: data.number, 114 title: data.title, 115 url: data.html_url, 116 state: data.state, 117 created_at: data.created_at 118 }) 119 }] 120 }; 121 } 122 123 if (request.params.name === "list_github_issues") { 124 const { owner, repo, state = "open", limit = 10 } = request.params.arguments as any; 125 126 const response = await fetch( 127 `https://api.github.com/repos/${owner}/${repo}/issues?state=${state}&per_page=${Math.min(limit, 100)}`, 128 { 129 headers: { 130 "Authorization": `token ${GITHUB_TOKEN}`, 131 "Accept": "application/vnd.github.v3+json" 132 }, 133 signal: AbortSignal.timeout(30000) 134 } 135 ); 136 137 if (!response.ok) { 138 throw new Error(`GitHub API error: ${response.statusText}`); 139 } 140 141 const issues = await response.json(); 142 return { 143 content: [{ 144 type: "text", 145 text: JSON.stringify(issues.map((issue: any) => ({ 146 id: issue.id, 147 number: issue.number, 148 title: issue.title, 149 url: issue.html_url, 150 state: issue.state, 151 created_at: issue.created_at 152 }))) 153 }] 154 }; 155 } 156 157 throw new Error(`Unknown tool: ${request.params.name}`); 158}); 159 160async function main() { 161 const transport = new StdioServerTransport(); 162 await server.connect(transport); 163 console.error("GitHub MCP server running on stdio"); 164} 165 166main().catch(console.error);
Notion API Integration
FastMCP Example:
python1import os 2import httpx 3from fastmcp import FastMCP 4from pydantic import BaseModel 5from typing import Optional 6 7mcp = FastMCP("Notion MCP Server") 8 9NOTION_TOKEN = os.getenv("NOTION_TOKEN") 10if not NOTION_TOKEN: 11 raise ValueError("NOTION_TOKEN environment variable required") 12 13class CreatePageInput(BaseModel): 14 database_id: str 15 title: str 16 content: Optional[str] = None 17 18@mcp.tool() 19async def create_notion_page(input: CreatePageInput) -> dict: 20 """Create a new page in a Notion database""" 21 async with httpx.AsyncClient() as client: 22 response = await client.post( 23 "https://api.notion.com/v1/pages", 24 json={ 25 "parent": {"database_id": input.database_id}, 26 "properties": { 27 "Title": { 28 "title": [{"text": {"content": input.title}}] 29 } 30 }, 31 "children": [ 32 { 33 "object": "block", 34 "type": "paragraph", 35 "paragraph": { 36 "rich_text": [{"text": {"content": input.content or ""}}] 37 } 38 } 39 ] if input.content else [] 40 }, 41 headers={ 42 "Authorization": f"Bearer {NOTION_TOKEN}", 43 "Notion-Version": "2022-06-28", 44 "Content-Type": "application/json" 45 }, 46 timeout=30.0 47 ) 48 49 if response.status_code == 401: 50 raise ValueError("Invalid NOTION_TOKEN") 51 elif response.status_code == 404: 52 raise ValueError(f"Database {input.database_id} not found") 53 54 response.raise_for_status() 55 return response.json()
MCP SDK Example:
typescript1const NOTION_TOKEN = process.env.NOTION_TOKEN; 2if (!NOTION_TOKEN) { 3 throw new Error("NOTION_TOKEN environment variable required"); 4} 5 6server.setRequestHandler(CallToolRequestSchema, async (request) => { 7 if (request.params.name === "create_notion_page") { 8 const { database_id, title, content } = request.params.arguments as any; 9 10 const response = await fetch("https://api.notion.com/v1/pages", { 11 method: "POST", 12 headers: { 13 "Authorization": `Bearer ${NOTION_TOKEN}`, 14 "Notion-Version": "2022-06-28", 15 "Content-Type": "application/json" 16 }, 17 body: JSON.stringify({ 18 parent: { database_id }, 19 properties: { 20 Title: { 21 title: [{ text: { content: title } }] 22 } 23 }, 24 children: content ? [{ 25 object: "block", 26 type: "paragraph", 27 paragraph: { 28 rich_text: [{ text: { content } }] 29 } 30 }] : [] 31 }), 32 signal: AbortSignal.timeout(30000) 33 }); 34 35 if (!response.ok) { 36 throw new Error(`Notion API error: ${response.statusText}`); 37 } 38 39 const data = await response.json(); 40 return { 41 content: [{ type: "text", text: JSON.stringify(data) }] 42 }; 43 } 44});
Database Connection Example
FastMCP Example:
python1import asyncpg 2from fastmcp import FastMCP 3from contextlib import asynccontextmanager 4import os 5 6mcp = FastMCP("Database MCP Server") 7 8DATABASE_URL = os.getenv("DATABASE_URL") 9if not DATABASE_URL: 10 raise ValueError("DATABASE_URL environment variable required") 11 12pool: asyncpg.Pool = None 13 14@asynccontextmanager 15async def lifespan(app): 16 global pool 17 pool = await asyncpg.create_pool( 18 DATABASE_URL, 19 min_size=2, 20 max_size=10, 21 command_timeout=30 22 ) 23 yield 24 await pool.close() 25 26mcp.lifespan = lifespan 27 28@mcp.tool() 29async def query_users(limit: int = 10, offset: int = 0) -> list[dict]: 30 """Query users from database with pagination""" 31 async with pool.acquire() as conn: 32 rows = await conn.fetch( 33 "SELECT id, name, email, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2", 34 limit, 35 offset 36 ) 37 return [dict(row) for row in rows] 38 39@mcp.tool() 40async def create_user(name: str, email: str) -> dict: 41 """Create a new user in the database""" 42 async with pool.acquire() as conn: 43 row = await conn.fetchrow( 44 "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email, created_at", 45 name, 46 email 47 ) 48 return dict(row)
MCP SDK Example:
typescript1import { Pool } from "pg"; 2 3const pool = new Pool({ 4 connectionString: process.env.DATABASE_URL, 5 min: 2, 6 max: 10, 7 connectionTimeoutMillis: 30000 8}); 9 10server.setRequestHandler(CallToolRequestSchema, async (request) => { 11 if (request.params.name === "query_users") { 12 const { limit = 10, offset = 0 } = request.params.arguments as any; 13 const client = await pool.connect(); 14 try { 15 const result = await client.query( 16 "SELECT id, name, email, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2", 17 [limit, offset] 18 ); 19 return { 20 content: [{ type: "text", text: JSON.stringify(result.rows) }] 21 }; 22 } finally { 23 client.release(); 24 } 25 } 26 27 if (request.params.name === "create_user") { 28 const { name, email } = request.params.arguments as any; 29 const client = await pool.connect(); 30 try { 31 const result = await client.query( 32 "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email, created_at", 33 [name, email] 34 ); 35 return { 36 content: [{ type: "text", text: JSON.stringify(result.rows[0]) }] 37 }; 38 } finally { 39 client.release(); 40 } 41 } 42});
File Operations Example
FastMCP Example:
python1from pathlib import Path 2from fastmcp import FastMCP 3from pydantic import BaseModel, Field 4import os 5 6mcp = FastMCP("File System MCP Server") 7 8WORKSPACE_ROOT = Path(os.getenv("WORKSPACE_ROOT", ".")).resolve() 9 10class ReadFileInput(BaseModel): 11 file_path: str = Field(..., description="Relative path from workspace root") 12 13def validate_path(file_path: str) -> Path: 14 """Validate and resolve file path safely""" 15 resolved = (WORKSPACE_ROOT / file_path).resolve() 16 17 # Security: Prevent directory traversal 18 if not str(resolved).startswith(str(WORKSPACE_ROOT)): 19 raise ValueError(f"Access denied: path outside workspace") 20 21 if not resolved.exists(): 22 raise FileNotFoundError(f"File not found: {file_path}") 23 24 if not resolved.is_file(): 25 raise ValueError(f"Path is not a file: {file_path}") 26 27 return resolved 28 29@mcp.tool() 30async def read_file(input: ReadFileInput) -> dict: 31 """Read file contents safely""" 32 path = validate_path(input.file_path) 33 return { 34 "content": path.read_text(), 35 "size": path.stat().st_size, 36 "path": str(path.relative_to(WORKSPACE_ROOT)) 37 } 38 39@mcp.tool() 40async def write_file(file_path: str, content: str) -> dict: 41 """Write file contents safely""" 42 path = validate_path(file_path) 43 path.write_text(content) 44 return { 45 "success": True, 46 "path": str(path.relative_to(WORKSPACE_ROOT)), 47 "size": len(content) 48 } 49 50@mcp.tool() 51async def list_directory(dir_path: str = ".") -> list[dict]: 52 """List directory contents""" 53 resolved = (WORKSPACE_ROOT / dir_path).resolve() 54 55 if not str(resolved).startswith(str(WORKSPACE_ROOT)): 56 raise ValueError("Access denied: path outside workspace") 57 58 if not resolved.exists(): 59 raise FileNotFoundError(f"Directory not found: {dir_path}") 60 61 if not resolved.is_dir(): 62 raise ValueError(f"Path is not a directory: {dir_path}") 63 64 return [ 65 { 66 "name": item.name, 67 "path": str(item.relative_to(WORKSPACE_ROOT)), 68 "type": "directory" if item.is_dir() else "file", 69 "size": item.stat().st_size if item.is_file() else None 70 } 71 for item in resolved.iterdir() 72 ]
MCP SDK Example:
typescript1import * as fs from "fs/promises"; 2import * as path from "path"; 3 4const WORKSPACE_ROOT = path.resolve(process.env.WORKSPACE_ROOT || "."); 5 6function validatePath(filePath: string): string { 7 const resolved = path.resolve(WORKSPACE_ROOT, filePath); 8 9 if (!resolved.startsWith(WORKSPACE_ROOT)) { 10 throw new Error("Access denied: path outside workspace"); 11 } 12 13 return resolved; 14} 15 16server.setRequestHandler(CallToolRequestSchema, async (request) => { 17 if (request.params.name === "read_file") { 18 const { file_path } = request.params.arguments as any; 19 const resolved = validatePath(file_path); 20 21 const stats = await fs.stat(resolved); 22 if (!stats.isFile()) { 23 throw new Error("Path is not a file"); 24 } 25 26 const content = await fs.readFile(resolved, "utf-8"); 27 return { 28 content: [{ 29 type: "text", 30 text: JSON.stringify({ 31 content, 32 size: stats.size, 33 path: path.relative(WORKSPACE_ROOT, resolved) 34 }) 35 }] 36 }; 37 } 38 39 if (request.params.name === "write_file") { 40 const { file_path, content } = request.params.arguments as any; 41 const resolved = validatePath(file_path); 42 43 await fs.writeFile(resolved, content, "utf-8"); 44 return { 45 content: [{ 46 type: "text", 47 text: JSON.stringify({ 48 success: true, 49 path: path.relative(WORKSPACE_ROOT, resolved), 50 size: content.length 51 }) 52 }] 53 }; 54 } 55 56 if (request.params.name === "list_directory") { 57 const { dir_path = "." } = request.params.arguments as any; 58 const resolved = validatePath(dir_path); 59 60 const stats = await fs.stat(resolved); 61 if (!stats.isDir()) { 62 throw new Error("Path is not a directory"); 63 } 64 65 const items = await fs.readdir(resolved); 66 const results = await Promise.all( 67 items.map(async (item) => { 68 const itemPath = path.join(resolved, item); 69 const itemStats = await fs.stat(itemPath); 70 return { 71 name: item, 72 path: path.relative(WORKSPACE_ROOT, itemPath), 73 type: itemStats.isDirectory() ? "directory" : "file", 74 size: itemStats.isFile() ? itemStats.size : null 75 }; 76 }) 77 ); 78 79 return { 80 content: [{ type: "text", text: JSON.stringify(results) }] 81 }; 82 } 83});
Implementation Quality Checklist
Use this checklist to validate your MCP server implementation:
Tool Design
- Each tool has a single, clear responsibility
- Tool names are descriptive and action-oriented
- Input parameters are validated with proper types
- Output schemas are well-defined
- Tools handle edge cases appropriately
Error Handling
- All errors are caught and handled appropriately
- Error messages are user-friendly and actionable
- Different error types are distinguished
- Retry logic is implemented where appropriate
- Timeouts are set for all external calls
Security
- API keys are stored in environment variables
- Input is sanitized and validated
- SQL injection is prevented (parameterized queries)
- Path traversal is prevented
- Rate limiting is implemented
- Permissions are scoped appropriately
Testing
- Unit tests cover all tools
- Integration tests validate end-to-end flows
- Error cases are tested
- Authentication flows are tested
- Rate limiting is tested
Documentation
- Tool descriptions are clear and LLM-friendly
- Parameters are documented with examples
- Return value schemas are documented
- Common pitfalls are documented
- Usage examples are provided
Production Readiness
- Configuration is managed via environment variables
- Dependencies are properly versioned
- Logging is implemented
- Monitoring is set up
- Versioning strategy is defined
- Backwards compatibility is maintained
Decision Trees
When to Create a New Tool vs Add Parameters
Is it a different operation?
├─ YES → Create new tool
└─ NO → Is it the same operation with different filters/options?
├─ YES → Add parameters to existing tool
└─ NO → Consider if operations are related
├─ Related and often used together → Consider combining
└─ Unrelated → Create separate tools
Error Handling Strategy
Error occurs
├─ Is it a validation error?
│ ├─ YES → Return ValueError with clear message
│ └─ NO → Is it a transient error (network, timeout)?
│ ├─ YES → Implement retry with exponential backoff
│ └─ NO → Is it a permanent error (404, 403)?
│ ├─ YES → Return specific error type with actionable message
│ └─ NO → Log and return generic error
Transport Layer Selection
What is your use case?
├─ Local development/CLI → Use stdio
├─ Web service/remote → Use HTTP
├─ Real-time updates needed → Use SSE
└─ Need bidirectional streaming → Consider HTTP with WebSockets
Conclusion
This guide provides comprehensive, actionable guidance for building production-ready MCP servers. Follow these patterns and principles to create maintainable, secure, and reliable MCP servers that integrate seamlessly with Claude Desktop and other MCP clients.
Remember:
- Start simple: Begin with atomic, single-purpose tools
- Validate everything: Input validation prevents security issues
- Handle errors gracefully: User-friendly error messages improve UX
- Test thoroughly: Comprehensive tests catch issues early
- Document clearly: Good documentation helps LLMs use your tools effectively
- Plan for production: Consider monitoring, logging, and deployment from the start
For additional resources:
- FastMCP Documentation: https://github.com/jlowin/fastmcp
- MCP SDK Documentation: https://modelcontextprotocol.io
- MCP Specification: https://spec.modelcontextprotocol.io